/*
 * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.image.impl;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.List;
import org.activiti.bpmn.model.AssociationDirection;
import org.activiti.bpmn.model.EventSubProcess;
import org.activiti.bpmn.model.GraphicInfo;
import org.activiti.bpmn.model.Transaction;
import org.activiti.image.exception.ActivitiImageException;
import org.activiti.image.impl.icon.BusinessRuleTaskIconType;
import org.activiti.image.impl.icon.CompensateIconType;
import org.activiti.image.impl.icon.CompensateThrowIconType;
import org.activiti.image.impl.icon.ErrorIconType;
import org.activiti.image.impl.icon.ErrorThrowIconType;
import org.activiti.image.impl.icon.IconType;
import org.activiti.image.impl.icon.LinkCatchIconType;
import org.activiti.image.impl.icon.LinkThrowIconType;
import org.activiti.image.impl.icon.ManualTaskIconType;
import org.activiti.image.impl.icon.MessageIconType;
import org.activiti.image.impl.icon.ReceiveTaskIconType;
import org.activiti.image.impl.icon.ScriptTaskIconType;
import org.activiti.image.impl.icon.SendTaskIconType;
import org.activiti.image.impl.icon.ServiceTaskIconType;
import org.activiti.image.impl.icon.SignalIconType;
import org.activiti.image.impl.icon.SignalThrowIconType;
import org.activiti.image.impl.icon.TaskIconType;
import org.activiti.image.impl.icon.TimerIconType;
import org.activiti.image.impl.icon.UserTaskIconType;
import org.apache.batik.dom.GenericDOMImplementation;
import org.apache.batik.svggen.SVGGraphics2DIOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;

/**
 * Represents a canvas on which BPMN 2.0 constructs can be drawn.
 * <p>
 * @see org.activiti.image.impl.DefaultProcessDiagramGenerator
 */
public class DefaultProcessDiagramCanvas {

    protected static final Logger LOGGER = LoggerFactory.getLogger(DefaultProcessDiagramCanvas.class);

    public enum SHAPE_TYPE {
        Rectangle,
        Rhombus,
        Ellipse,
    }

    // Predefined sized
    protected static final int ARROW_WIDTH = 5;
    protected static final int CONDITIONAL_INDICATOR_WIDTH = 16;
    protected static final int DEFAULT_INDICATOR_WIDTH = 10;
    protected static final int MARKER_WIDTH = 12;
    protected static final int FONT_SIZE = 11;
    protected static final int FONT_SPACING = 2;
    protected static final int TEXT_PADDING = 3;
    protected static final int ANNOTATION_TEXT_PADDING = 7;
    protected static final int LINE_HEIGHT = FONT_SIZE + FONT_SPACING;

    // Colors
    protected static Color TASK_BOX_COLOR = new Color(249, 249, 249);
    protected static Color SUBPROCESS_BOX_COLOR = new Color(255, 255, 255);
    protected static Color EVENT_COLOR = new Color(255, 255, 255);
    protected static Color CONNECTION_COLOR = new Color(88, 88, 88);
    protected static Color CONDITIONAL_INDICATOR_COLOR = new Color(255, 255, 255);
    protected static Color HIGHLIGHT_CURRENT_COLOR = new Color(87, 255, 174);
    protected static Color HIGHLIGHT_COMPLETED_ACTIVITY_COLOR = new Color(51, 153, 255);
    protected static Color HIGHLIGHT_ERRORED_ACTIVITY_COLOR = new Color(255, 55, 87);
    protected static Color LABEL_COLOR = new Color(112, 146, 190);
    protected static Color TASK_BORDER_COLOR = new Color(187, 187, 187);
    protected static Color EVENT_BORDER_COLOR = new Color(88, 88, 88);
    protected static Color SUBPROCESS_BORDER_COLOR = new Color(0, 0, 0);

    // Fonts
    protected static Font LABEL_FONT = null;
    protected static Font ANNOTATION_FONT = null;

    // Strokes
    protected static Stroke THICK_TASK_BORDER_STROKE = new BasicStroke(4.0f);
    protected static Stroke GATEWAY_TYPE_STROKE = new BasicStroke(3.0f);
    protected static Stroke END_EVENT_STROKE = new BasicStroke(3.0f);
    protected static Stroke MULTI_INSTANCE_STROKE = new BasicStroke(1.3f);
    protected static Stroke EVENT_SUBPROCESS_STROKE = new BasicStroke(
        1.0f,
        BasicStroke.CAP_BUTT,
        BasicStroke.JOIN_MITER,
        1.0f,
        new float[] { 1.0f },
        0.0f
    );
    protected static Stroke NON_INTERRUPTING_EVENT_STROKE = new BasicStroke(
        1.0f,
        BasicStroke.CAP_BUTT,
        BasicStroke.JOIN_MITER,
        1.0f,
        new float[] { 4.0f, 3.0f },
        0.0f
    );
    protected static Stroke HIGHLIGHT_FLOW_STROKE = new BasicStroke(2.0f);
    protected static Stroke ANNOTATION_STROKE = new BasicStroke(2.0f);
    protected static Stroke ASSOCIATION_STROKE = new BasicStroke(
        2.0f,
        BasicStroke.CAP_BUTT,
        BasicStroke.JOIN_MITER,
        1.0f,
        new float[] { 2.0f, 2.0f },
        0.0f
    );

    // icons
    protected static int ICON_PADDING = 5;
    protected static TaskIconType USERTASK_IMAGE;
    protected static TaskIconType SCRIPTTASK_IMAGE;
    protected static TaskIconType SERVICETASK_IMAGE;
    protected static TaskIconType RECEIVETASK_IMAGE;
    protected static TaskIconType SENDTASK_IMAGE;
    protected static TaskIconType MANUALTASK_IMAGE;
    protected static TaskIconType BUSINESS_RULE_TASK_IMAGE;

    protected static IconType TIMER_IMAGE;
    protected static IconType COMPENSATE_THROW_IMAGE;
    protected static IconType COMPENSATE_CATCH_IMAGE;
    protected static IconType ERROR_THROW_IMAGE;
    protected static IconType ERROR_CATCH_IMAGE;
    protected static IconType MESSAGE_CATCH_IMAGE;
    protected static IconType SIGNAL_CATCH_IMAGE;
    protected static IconType SIGNAL_THROW_IMAGE;
    protected static IconType LINK_CATCH_IMAGE;
    protected static IconType LINK_THROW_IMAGE;

    protected int canvasWidth = -1;
    protected int canvasHeight = -1;
    protected int minX = -1;
    protected int minY = -1;
    protected ProcessDiagramSVGGraphics2D g;
    protected FontMetrics fontMetrics;
    protected boolean closed;
    protected String activityFontName = "Arial";
    protected String labelFontName = "Arial";
    protected String annotationFontName = "Arial";

    /**
     * Creates an empty canvas with given width and height.
     * <p>
     * Allows to specify minimal boundaries on the left and upper side of the
     * canvas. This is useful for diagrams that have white space there.
     * Everything beneath these minimum values will be cropped.
     * It's also possible to pass a specific font name and a class loader for the icon images.
     */
    public DefaultProcessDiagramCanvas(
        int width,
        int height,
        int minX,
        int minY,
        String activityFontName,
        String labelFontName,
        String annotationFontName
    ) {
        this.canvasWidth = width;
        this.canvasHeight = height;
        this.minX = minX;
        this.minY = minY;
        if (activityFontName != null) {
            this.activityFontName = activityFontName;
        }
        if (labelFontName != null) {
            this.labelFontName = labelFontName;
        }
        if (annotationFontName != null) {
            this.annotationFontName = annotationFontName;
        }

        initialize();
    }

    /**
     * Creates an empty canvas with given width and height.
     * <p>
     * Allows to specify minimal boundaries on the left and upper side of the
     * canvas. This is useful for diagrams that have white space there (eg
     * Signavio). Everything beneath these minimum values will be cropped.
     * @param minX Hint that will be used when generating the image. Parts that fall
     * below minX on the horizontal scale will be cropped.
     * @param minY Hint that will be used when generating the image. Parts that fall
     * below minX on the horizontal scale will be cropped.
     */
    public DefaultProcessDiagramCanvas(int width, int height, int minX, int minY) {
        this.canvasWidth = width;
        this.canvasHeight = height;
        this.minX = minX;
        this.minY = minY;

        initialize();
    }

    public void initialize() {
        // Get a DOMImplementation.
        DOMImplementation domImpl = GenericDOMImplementation.getDOMImplementation();

        // Create an instance of org.w3c.dom.Document.
        String svgNS = "http://www.w3.org/2000/svg";
        Document document = domImpl.createDocument(svgNS, "svg", null);

        // Create an instance of the SVG Generator.
        this.g = new ProcessDiagramSVGGraphics2D(document);

        this.g.setSVGCanvasSize(new Dimension(this.canvasWidth, this.canvasHeight));

        this.g.setBackground(new Color(255, 255, 255, 0));
        this.g.clearRect(0, 0, canvasWidth, canvasHeight);

        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.setPaint(Color.black);

        Font font = new Font(activityFontName, Font.BOLD, FONT_SIZE);
        g.setFont(font);
        this.fontMetrics = g.getFontMetrics();

        LABEL_FONT = new Font(labelFontName, Font.ITALIC, 10);
        ANNOTATION_FONT = new Font(annotationFontName, Font.PLAIN, FONT_SIZE);

        USERTASK_IMAGE = new UserTaskIconType();
        SCRIPTTASK_IMAGE = new ScriptTaskIconType();
        SERVICETASK_IMAGE = new ServiceTaskIconType();
        RECEIVETASK_IMAGE = new ReceiveTaskIconType();
        SENDTASK_IMAGE = new SendTaskIconType();
        MANUALTASK_IMAGE = new ManualTaskIconType();
        BUSINESS_RULE_TASK_IMAGE = new BusinessRuleTaskIconType();

        TIMER_IMAGE = new TimerIconType();
        COMPENSATE_THROW_IMAGE = new CompensateThrowIconType();
        COMPENSATE_CATCH_IMAGE = new CompensateIconType();
        ERROR_THROW_IMAGE = new ErrorThrowIconType();
        ERROR_CATCH_IMAGE = new ErrorIconType();
        MESSAGE_CATCH_IMAGE = new MessageIconType();
        SIGNAL_THROW_IMAGE = new SignalThrowIconType();
        SIGNAL_CATCH_IMAGE = new SignalIconType();
        LINK_THROW_IMAGE = new LinkThrowIconType();
        LINK_CATCH_IMAGE = new LinkCatchIconType();
    }

    /**
     * Generates an image of what currently is drawn on the canvas.
     * <p>
     * Throws an {@link ActivitiImageException} when {@link #close()} is already
     * called.
     */
    public InputStream generateImage() {
        if (closed) {
            throw new ActivitiImageException("ProcessDiagramGenerator already closed");
        }

        try {
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            Writer out;
            out = new OutputStreamWriter(stream, "UTF-8");
            g.stream(out, true);
            return new ByteArrayInputStream(stream.toByteArray());
        } catch (UnsupportedEncodingException | SVGGraphics2DIOException e) {
            throw new ActivitiImageException("Error while generating process image", e);
        }
    }

    /**
     * Closes the canvas which dissallows further drawing and releases graphical
     * resources.
     */
    public void close() {
        g.dispose();
        closed = true;
    }

    public void drawNoneStartEvent(String id, String name, GraphicInfo graphicInfo) {
        drawStartEvent(id, name, graphicInfo, null);
    }

    public void drawTimerStartEvent(String id, String name, GraphicInfo graphicInfo) {
        drawStartEvent(id, name, graphicInfo, TIMER_IMAGE);
    }

    public void drawSignalStartEvent(String id, String name, GraphicInfo graphicInfo) {
        drawStartEvent(id, name, graphicInfo, SIGNAL_CATCH_IMAGE);
    }

    public void drawMessageStartEvent(String id, String name, GraphicInfo graphicInfo) {
        drawStartEvent(id, name, graphicInfo, MESSAGE_CATCH_IMAGE);
    }

    public void drawStartEvent(String id, String name, GraphicInfo graphicInfo, IconType icon) {
        Paint originalPaint = g.getPaint();
        g.setPaint(EVENT_COLOR);
        Ellipse2D circle = new Ellipse2D.Double(
            graphicInfo.getX(),
            graphicInfo.getY(),
            graphicInfo.getWidth(),
            graphicInfo.getHeight()
        );
        g.fill(circle);
        g.setPaint(EVENT_BORDER_COLOR);
        g.draw(circle);
        g.setPaint(originalPaint);

        // calculate coordinates to center image
        if (icon != null) {
            int imageX = (int) Math.round(graphicInfo.getX() + (graphicInfo.getWidth() / 2) - (icon.getWidth() / 2));
            int imageY = (int) Math.round(graphicInfo.getY() + (graphicInfo.getHeight() / 2) - (icon.getHeight() / 2));

            icon.drawIcon(imageX, imageY, ICON_PADDING, g);
        }

        // set element's id
        g.setCurrentGroupId(id);
        drawLabel(name, graphicInfo);
    }

    public void drawNoneEndEvent(String id, String name, GraphicInfo graphicInfo) {
        Paint originalPaint = g.getPaint();
        Stroke originalStroke = g.getStroke();
        g.setPaint(EVENT_COLOR);
        Ellipse2D circle = new Ellipse2D.Double(
            graphicInfo.getX(),
            graphicInfo.getY(),
            graphicInfo.getWidth(),
            graphicInfo.getHeight()
        );
        g.fill(circle);
        g.setPaint(EVENT_BORDER_COLOR);
        g.setStroke(END_EVENT_STROKE);
        g.draw(circle);
        g.setStroke(originalStroke);
        g.setPaint(originalPaint);

        // set element's id
        g.setCurrentGroupId(id);

        drawLabel(name, graphicInfo);
    }

    public void drawErrorEndEvent(String id, String name, GraphicInfo graphicInfo) {
        drawNoneEndEvent(id, name, graphicInfo);

        int imageX = (int) (graphicInfo.getX() + (graphicInfo.getWidth() / 4));
        int imageY = (int) (graphicInfo.getY() + (graphicInfo.getHeight() / 4));

        ERROR_THROW_IMAGE.drawIcon(imageX, imageY, ICON_PADDING, g);
    }

    public void drawErrorStartEvent(String id, String name, GraphicInfo graphicInfo) {
        drawNoneStartEvent(id, name, graphicInfo);

        int imageX = (int) (graphicInfo.getX() + (graphicInfo.getWidth() / 4));
        int imageY = (int) (graphicInfo.getY() + (graphicInfo.getHeight() / 4));

        ERROR_THROW_IMAGE.drawIcon(imageX, imageY, ICON_PADDING, g);
    }

    public void drawCatchingEvent(
        String id,
        GraphicInfo graphicInfo,
        boolean isInterrupting,
        IconType icon,
        String eventType
    ) {
        // event circles
        Ellipse2D outerCircle = new Ellipse2D.Double(
            graphicInfo.getX(),
            graphicInfo.getY(),
            graphicInfo.getWidth(),
            graphicInfo.getHeight()
        );
        int innerCircleSize = 4;
        int innerCircleX = (int) graphicInfo.getX() + innerCircleSize;
        int innerCircleY = (int) graphicInfo.getY() + innerCircleSize;
        int innerCircleWidth = (int) graphicInfo.getWidth() - (2 * innerCircleSize);
        int innerCircleHeight = (int) graphicInfo.getHeight() - (2 * innerCircleSize);
        Ellipse2D innerCircle = new Ellipse2D.Double(innerCircleX, innerCircleY, innerCircleWidth, innerCircleHeight);

        Paint originalPaint = g.getPaint();
        Stroke originalStroke = g.getStroke();
        g.setPaint(EVENT_COLOR);
        g.fill(outerCircle);

        g.setPaint(EVENT_BORDER_COLOR);
        if (!isInterrupting) {
            g.setStroke(NON_INTERRUPTING_EVENT_STROKE);
        }
        g.draw(outerCircle);
        g.setStroke(originalStroke);
        g.setPaint(originalPaint);
        g.draw(innerCircle);

        if (icon != null) {
            // calculate coordinates to center image
            int imageX = (int) (graphicInfo.getX() + (graphicInfo.getWidth() / 2) - (icon.getWidth() / 2));
            int imageY = (int) (graphicInfo.getY() + (graphicInfo.getHeight() / 2) - (icon.getHeight() / 2));
            if ("timer".equals(eventType)) {
                // move image one pixel to center timer image
                imageX++;
                imageY++;
            }
            icon.drawIcon(imageX, imageY, ICON_PADDING, g);
        }

        // set element's id
        g.setCurrentGroupId(id);
    }

    public void drawCatchingCompensateEvent(String id, String name, GraphicInfo graphicInfo, boolean isInterrupting) {
        drawCatchingCompensateEvent(id, graphicInfo, isInterrupting);
        drawLabel(name, graphicInfo);
    }

    public void drawCatchingCompensateEvent(String id, GraphicInfo graphicInfo, boolean isInterrupting) {
        drawCatchingEvent(id, graphicInfo, isInterrupting, COMPENSATE_CATCH_IMAGE, "compensate");
    }

    public void drawCatchingTimerEvent(String id, String name, GraphicInfo graphicInfo, boolean isInterrupting) {
        drawCatchingTimerEvent(id, graphicInfo, isInterrupting);
        drawLabel(name, graphicInfo);
    }

    public void drawCatchingTimerEvent(String id, GraphicInfo graphicInfo, boolean isInterrupting) {
        drawCatchingEvent(id, graphicInfo, isInterrupting, TIMER_IMAGE, "timer");
    }

    public void drawCatchingErrorEvent(String id, String name, GraphicInfo graphicInfo, boolean isInterrupting) {
        drawCatchingErrorEvent(id, graphicInfo, isInterrupting);
        drawLabel(name, graphicInfo);
    }

    public void drawCatchingErrorEvent(String id, GraphicInfo graphicInfo, boolean isInterrupting) {
        drawCatchingEvent(id, graphicInfo, isInterrupting, ERROR_CATCH_IMAGE, "error");
    }

    public void drawCatchingSignalEvent(String id, String name, GraphicInfo graphicInfo, boolean isInterrupting) {
        drawCatchingSignalEvent(id, graphicInfo, isInterrupting);
        drawLabel(name, graphicInfo);
    }

    public void drawCatchingSignalEvent(String id, GraphicInfo graphicInfo, boolean isInterrupting) {
        drawCatchingEvent(id, graphicInfo, isInterrupting, SIGNAL_CATCH_IMAGE, "signal");
    }

    public void drawCatchingMessageEvent(String id, GraphicInfo graphicInfo, boolean isInterrupting) {
        drawCatchingEvent(id, graphicInfo, isInterrupting, MESSAGE_CATCH_IMAGE, "message");
    }

    public void drawCatchingMessageEvent(String id, String name, GraphicInfo graphicInfo, boolean isInterrupting) {
        drawCatchingEvent(id, graphicInfo, isInterrupting, MESSAGE_CATCH_IMAGE, "message");

        drawLabel(name, graphicInfo);
    }

    public void drawCatchingLinkEvent(String id, String name, GraphicInfo graphicInfo, boolean isInterrupting) {
        drawCatchingEvent(id, graphicInfo, isInterrupting, LINK_CATCH_IMAGE, "link");

        drawLabel(name, graphicInfo);
    }

    public void drawThrowingCompensateEvent(String id, String name, GraphicInfo graphicInfo) {
        drawCatchingEvent(id, graphicInfo, true, COMPENSATE_THROW_IMAGE, "compensate");
        drawLabel(name, graphicInfo);
    }

    public void drawThrowingSignalEvent(String id, String name, GraphicInfo graphicInfo) {
        drawCatchingEvent(id, graphicInfo, true, SIGNAL_THROW_IMAGE, "signal");
        drawLabel(name, graphicInfo);
    }

    public void drawThrowingLinkEvent(String id, String name, GraphicInfo graphicInfo) {
        drawCatchingEvent(id, graphicInfo, true, LINK_THROW_IMAGE, "link");
        drawLabel(name, graphicInfo);
    }

    public void drawThrowingNoneEvent(String id, String name, GraphicInfo graphicInfo) {
        drawCatchingEvent(id, graphicInfo, true, null, "none");
        drawLabel(name, graphicInfo);
    }

    public void drawSequenceflow(int srcX, int srcY, int targetX, int targetY, boolean conditional) {
        drawSequenceflow(srcX, srcY, targetX, targetY, conditional, false);
    }

    public void drawSequenceflow(
        int srcX,
        int srcY,
        int targetX,
        int targetY,
        boolean conditional,
        boolean highLighted
    ) {
        Paint originalPaint = g.getPaint();
        if (highLighted) {
            g.setPaint(HIGHLIGHT_CURRENT_COLOR);
        }

        Line2D.Double line = new Line2D.Double(srcX, srcY, targetX, targetY);
        g.draw(line);
        drawArrowHead(line);

        if (conditional) {
            drawConditionalSequenceFlowIndicator(line);
        }

        if (highLighted) {
            g.setPaint(originalPaint);
        }
    }

    public void drawAssociation(
        int[] xPoints,
        int[] yPoints,
        AssociationDirection associationDirection,
        boolean highLighted
    ) {
        boolean conditional = false;
        boolean isDefault = false;
        drawConnection(xPoints, yPoints, conditional, isDefault, "association", associationDirection, highLighted);
    }

    public void drawSequenceflow(
        int[] xPoints,
        int[] yPoints,
        boolean conditional,
        boolean isDefault,
        boolean highLighted
    ) {
        drawConnection(xPoints, yPoints, conditional, isDefault, "sequenceFlow", AssociationDirection.ONE, highLighted);
    }

    public void drawConnection(
        int[] xPoints,
        int[] yPoints,
        boolean conditional,
        boolean isDefault,
        String connectionType,
        AssociationDirection associationDirection,
        boolean highLighted
    ) {
        Paint originalPaint = g.getPaint();
        Stroke originalStroke = g.getStroke();

        g.setPaint(CONNECTION_COLOR);
        if ("association".equals(connectionType)) {
            g.setStroke(ASSOCIATION_STROKE);
        } else if (highLighted) {
            g.setPaint(HIGHLIGHT_COMPLETED_ACTIVITY_COLOR);
            g.setStroke(HIGHLIGHT_FLOW_STROKE);
        }

        for (int i = 1; i < xPoints.length; i++) {
            Integer sourceX = xPoints[i - 1];
            Integer sourceY = yPoints[i - 1];
            Integer targetX = xPoints[i];
            Integer targetY = yPoints[i];
            Line2D.Double line = new Line2D.Double(sourceX, sourceY, targetX, targetY);
            g.draw(line);
        }

        if (isDefault) {
            Line2D.Double line = new Line2D.Double(xPoints[0], yPoints[0], xPoints[1], yPoints[1]);
            drawDefaultSequenceFlowIndicator(line);
        }

        if (conditional) {
            Line2D.Double line = new Line2D.Double(xPoints[0], yPoints[0], xPoints[1], yPoints[1]);
            drawConditionalSequenceFlowIndicator(line);
        }

        if (
            associationDirection.equals(AssociationDirection.ONE) ||
            associationDirection.equals(AssociationDirection.BOTH)
        ) {
            Line2D.Double line = new Line2D.Double(
                xPoints[xPoints.length - 2],
                yPoints[xPoints.length - 2],
                xPoints[xPoints.length - 1],
                yPoints[xPoints.length - 1]
            );
            drawArrowHead(line);
        }
        if (associationDirection.equals(AssociationDirection.BOTH)) {
            Line2D.Double line = new Line2D.Double(xPoints[1], yPoints[1], xPoints[0], yPoints[0]);
            drawArrowHead(line);
        }
        g.setPaint(originalPaint);
        g.setStroke(originalStroke);
    }

    public void drawSequenceflowWithoutArrow(int srcX, int srcY, int targetX, int targetY, boolean conditional) {
        drawSequenceflowWithoutArrow(srcX, srcY, targetX, targetY, conditional, false);
    }

    public void drawSequenceflowWithoutArrow(
        int srcX,
        int srcY,
        int targetX,
        int targetY,
        boolean conditional,
        boolean highLighted
    ) {
        Paint originalPaint = g.getPaint();
        if (highLighted) {
            g.setPaint(HIGHLIGHT_CURRENT_COLOR);
        }

        Line2D.Double line = new Line2D.Double(srcX, srcY, targetX, targetY);
        g.draw(line);

        if (conditional) {
            drawConditionalSequenceFlowIndicator(line);
        }

        if (highLighted) {
            g.setPaint(originalPaint);
        }
    }

    public void drawArrowHead(Line2D.Double line) {
        int doubleArrowWidth = (int) (2 * ARROW_WIDTH);
        if (doubleArrowWidth == 0) {
            doubleArrowWidth = 2;
        }
        Polygon arrowHead = new Polygon();
        arrowHead.addPoint(0, 0);
        int arrowHeadPoint = (int) (-ARROW_WIDTH);
        if (arrowHeadPoint == 0) {
            arrowHeadPoint = -1;
        }
        arrowHead.addPoint(arrowHeadPoint, -doubleArrowWidth);
        arrowHeadPoint = (int) (ARROW_WIDTH);
        if (arrowHeadPoint == 0) {
            arrowHeadPoint = 1;
        }
        arrowHead.addPoint(arrowHeadPoint, -doubleArrowWidth);

        AffineTransform transformation = new AffineTransform();
        transformation.setToIdentity();
        double angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1);
        transformation.translate(line.x2, line.y2);
        transformation.rotate((angle - Math.PI / 2d));

        AffineTransform originalTransformation = g.getTransform();
        g.setTransform(transformation);
        g.fill(arrowHead);
        g.setTransform(originalTransformation);
    }

    public void drawDefaultSequenceFlowIndicator(Line2D.Double line) {
        double length = DEFAULT_INDICATOR_WIDTH;
        double halfOfLength = length / 2;
        double f = 8;
        Line2D.Double defaultIndicator = new Line2D.Double(-halfOfLength, 0, halfOfLength, 0);

        double angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1);
        double dx = f * Math.cos(angle);
        double dy = f * Math.sin(angle);
        double x1 = line.x1 + dx;
        double y1 = line.y1 + dy;

        AffineTransform transformation = new AffineTransform();
        transformation.setToIdentity();
        transformation.translate(x1, y1);
        transformation.rotate((angle - (3 * Math.PI) / 4));

        AffineTransform originalTransformation = g.getTransform();
        g.setTransform(transformation);
        g.draw(defaultIndicator);

        g.setTransform(originalTransformation);
    }

    public void drawConditionalSequenceFlowIndicator(Line2D.Double line) {
        int horizontal = (int) (CONDITIONAL_INDICATOR_WIDTH * 0.7);
        int halfOfHorizontal = horizontal / 2;
        int halfOfVertical = CONDITIONAL_INDICATOR_WIDTH / 2;

        Polygon conditionalIndicator = new Polygon();
        conditionalIndicator.addPoint(0, 0);
        conditionalIndicator.addPoint(-halfOfHorizontal, halfOfVertical);
        conditionalIndicator.addPoint(0, CONDITIONAL_INDICATOR_WIDTH);
        conditionalIndicator.addPoint(halfOfHorizontal, halfOfVertical);

        AffineTransform transformation = new AffineTransform();
        transformation.setToIdentity();
        double angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1);
        transformation.translate(line.x1, line.y1);
        transformation.rotate((angle - Math.PI / 2d));

        AffineTransform originalTransformation = g.getTransform();
        g.setTransform(transformation);
        g.draw(conditionalIndicator);

        Paint originalPaint = g.getPaint();
        g.setPaint(CONDITIONAL_INDICATOR_COLOR);
        g.fill(conditionalIndicator);

        g.setPaint(originalPaint);
        g.setTransform(originalTransformation);
    }

    public void drawTask(TaskIconType icon, String id, String name, GraphicInfo graphicInfo) {
        drawTask(id, name, graphicInfo);

        icon.drawIcon((int) graphicInfo.getX(), (int) graphicInfo.getY(), ICON_PADDING, g);
    }

    public void drawTask(String id, String name, GraphicInfo graphicInfo) {
        drawTask(id, name, graphicInfo, false);
    }

    public void drawPoolOrLane(String id, String name, GraphicInfo graphicInfo) {
        int x = (int) graphicInfo.getX();
        int y = (int) graphicInfo.getY();
        int width = (int) graphicInfo.getWidth();
        int height = (int) graphicInfo.getHeight();
        g.drawRect(x, y, width, height);

        // Add the name as text, vertical
        if (name != null && name.length() > 0) {
            // Include some padding
            int availableTextSpace = height - 6;

            // Create rotation for derived font
            AffineTransform transformation = new AffineTransform();
            transformation.setToIdentity();
            transformation.rotate((270 * Math.PI) / 180);

            Font currentFont = g.getFont();
            Font theDerivedFont = currentFont.deriveFont(transformation);
            g.setFont(theDerivedFont);

            String truncated = fitTextToWidth(name, availableTextSpace);
            int realWidth = fontMetrics.stringWidth(truncated);

            g.drawString(
                truncated,
                x + 2 + fontMetrics.getHeight(),
                3 + y + availableTextSpace - (availableTextSpace - realWidth) / 2
            );
            g.setFont(currentFont);
        }

        // set element's id
        g.setCurrentGroupId(id);
    }

    protected void drawTask(String id, String name, GraphicInfo graphicInfo, boolean thickBorder) {
        Paint originalPaint = g.getPaint();
        int x = (int) graphicInfo.getX();
        int y = (int) graphicInfo.getY();
        int width = (int) graphicInfo.getWidth();
        int height = (int) graphicInfo.getHeight();

        // Create a new gradient paint for every task box, gradient depends on x and y and is not relative
        g.setPaint(TASK_BOX_COLOR);

        int arcR = 6;
        if (thickBorder) {
            arcR = 3;
        }

        // shape
        RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, arcR, arcR);
        g.fill(rect);
        g.setPaint(TASK_BORDER_COLOR);

        if (thickBorder) {
            Stroke originalStroke = g.getStroke();
            g.setStroke(THICK_TASK_BORDER_STROKE);
            g.draw(rect);
            g.setStroke(originalStroke);
        } else {
            g.draw(rect);
        }

        g.setPaint(originalPaint);
        // text
        if (name != null && name.length() > 0) {
            int boxWidth = width - (2 * TEXT_PADDING);
            int boxHeight = height - 16 - ICON_PADDING - ICON_PADDING - MARKER_WIDTH - 2 - 2;
            int boxX = x + width / 2 - boxWidth / 2;
            int boxY = y + height / 2 - boxHeight / 2 + ICON_PADDING + ICON_PADDING - 2 - 2;

            drawMultilineCentredText(name, boxX, boxY, boxWidth, boxHeight);
        }

        // set element's id
        g.setCurrentGroupId(id);
    }

    protected void drawMultilineCentredText(String text, int x, int y, int boxWidth, int boxHeight) {
        drawMultilineText(text, x, y, boxWidth, boxHeight, true);
    }

    protected void drawMultilineAnnotationText(String text, int x, int y, int boxWidth, int boxHeight) {
        drawMultilineText(text, x, y, boxWidth, boxHeight, false);
    }

    protected void drawMultilineText(String text, int x, int y, int boxWidth, int boxHeight, boolean centered) {
        // Create an attributed string based in input text
        AttributedString attributedString = new AttributedString(text);
        attributedString.addAttribute(TextAttribute.FONT, g.getFont());
        attributedString.addAttribute(TextAttribute.FOREGROUND, Color.black);

        AttributedCharacterIterator characterIterator = attributedString.getIterator();

        int currentHeight = 0;
        // Prepare a list of lines of text we'll be drawing
        List<TextLayout> layouts = new ArrayList<TextLayout>();
        String lastLine = null;

        LineBreakMeasurer measurer = new LineBreakMeasurer(characterIterator, g.getFontRenderContext());

        TextLayout layout = null;
        while (measurer.getPosition() < characterIterator.getEndIndex() && currentHeight <= boxHeight) {
            int previousPosition = measurer.getPosition();

            // Request next layout
            layout = measurer.nextLayout(boxWidth);

            int height = ((Float) (layout.getDescent() + layout.getAscent() + layout.getLeading())).intValue();

            if (currentHeight + height > boxHeight) {
                // The line we're about to add should NOT be added anymore, append three dots to previous one instead
                // to indicate more text is truncated
                if (!layouts.isEmpty()) {
                    layouts.remove(layouts.size() - 1);

                    if (lastLine.length() >= 4) {
                        lastLine = lastLine.substring(0, lastLine.length() - 4) + "...";
                    }
                    layouts.add(new TextLayout(lastLine, g.getFont(), g.getFontRenderContext()));
                } else {
                    // at least, draw one line
                    // even if text does not fit
                    // in order to avoid empty box
                    layouts.add(layout);
                    currentHeight += height;
                }
                break;
            } else {
                layouts.add(layout);
                lastLine = text.substring(previousPosition, measurer.getPosition());
                currentHeight += height;
            }
        }

        int currentY = y + (centered ? ((boxHeight - currentHeight) / 2) : 0);
        int currentX = 0;

        // Actually draw the lines
        for (TextLayout textLayout : layouts) {
            currentY += textLayout.getAscent();
            currentX = x + (centered ? ((boxWidth - ((Double) textLayout.getBounds().getWidth()).intValue()) / 2) : 0);

            textLayout.draw(g, currentX, currentY);
            currentY += textLayout.getDescent() + textLayout.getLeading();
        }
    }

    protected String fitTextToWidth(String original, int width) {
        String text = original;

        // remove length for "..."
        int maxWidth = width - 10;

        while (fontMetrics.stringWidth(text + "...") > maxWidth && text.length() > 0) {
            text = text.substring(0, text.length() - 1);
        }

        if (!text.equals(original)) {
            text = text + "...";
        }

        return text;
    }

    public void drawUserTask(String id, String name, GraphicInfo graphicInfo) {
        drawTask(USERTASK_IMAGE, id, name, graphicInfo);
    }

    public void drawScriptTask(String id, String name, GraphicInfo graphicInfo) {
        drawTask(SCRIPTTASK_IMAGE, id, name, graphicInfo);
    }

    public void drawServiceTask(String id, String name, GraphicInfo graphicInfo) {
        drawTask(SERVICETASK_IMAGE, id, name, graphicInfo);
    }

    public void drawReceiveTask(String id, String name, GraphicInfo graphicInfo) {
        drawTask(RECEIVETASK_IMAGE, id, name, graphicInfo);
    }

    public void drawSendTask(String id, String name, GraphicInfo graphicInfo) {
        drawTask(SENDTASK_IMAGE, id, name, graphicInfo);
    }

    public void drawManualTask(String id, String name, GraphicInfo graphicInfo) {
        drawTask(MANUALTASK_IMAGE, id, name, graphicInfo);
    }

    public void drawBusinessRuleTask(String id, String name, GraphicInfo graphicInfo) {
        drawTask(BUSINESS_RULE_TASK_IMAGE, id, name, graphicInfo);
    }

    public void drawExpandedSubProcess(String id, String name, GraphicInfo graphicInfo, Class<?> type) {
        RoundRectangle2D rect = new RoundRectangle2D.Double(
            graphicInfo.getX(),
            graphicInfo.getY(),
            graphicInfo.getWidth(),
            graphicInfo.getHeight(),
            8,
            8
        );

        if (type.equals(EventSubProcess.class)) {
            Stroke originalStroke = g.getStroke();
            g.setStroke(EVENT_SUBPROCESS_STROKE);
            g.draw(rect);
            g.setStroke(originalStroke);
        } else if (type.equals(Transaction.class)) {
            RoundRectangle2D outerRect = new RoundRectangle2D.Double(
                graphicInfo.getX() - 3,
                graphicInfo.getY() - 3,
                graphicInfo.getWidth() + 6,
                graphicInfo.getHeight() + 6,
                8,
                8
            );

            Paint originalPaint = g.getPaint();
            g.setPaint(SUBPROCESS_BOX_COLOR);
            g.fill(outerRect);
            g.setPaint(SUBPROCESS_BORDER_COLOR);
            g.draw(outerRect);
            g.setPaint(SUBPROCESS_BOX_COLOR);
            g.fill(rect);
            g.setPaint(SUBPROCESS_BORDER_COLOR);
            g.draw(rect);
            g.setPaint(originalPaint);
        } else {
            Paint originalPaint = g.getPaint();
            g.setPaint(SUBPROCESS_BOX_COLOR);
            g.fill(rect);
            g.setPaint(SUBPROCESS_BORDER_COLOR);
            g.draw(rect);
            g.setPaint(originalPaint);
        }

        if (name != null && !name.isEmpty()) {
            String text = fitTextToWidth(name, (int) graphicInfo.getWidth());
            g.drawString(text, (int) graphicInfo.getX() + 10, (int) graphicInfo.getY() + 15);
        }

        // set element's id
        g.setCurrentGroupId(id);
    }

    public void drawCollapsedSubProcess(String id, String name, GraphicInfo graphicInfo, Boolean isTriggeredByEvent) {
        drawCollapsedTask(id, name, graphicInfo, false);
    }

    public void drawCollapsedCallActivity(String id, String name, GraphicInfo graphicInfo) {
        drawCollapsedTask(id, name, graphicInfo, true);
    }

    protected void drawCollapsedTask(String id, String name, GraphicInfo graphicInfo, boolean thickBorder) {
        // The collapsed marker is now visualized separately
        drawTask(id, name, graphicInfo, thickBorder);
    }

    public void drawCollapsedMarker(int x, int y, int width, int height) {
        // rectangle
        int rectangleWidth = MARKER_WIDTH;
        int rectangleHeight = MARKER_WIDTH;
        Rectangle rect = new Rectangle(
            x + (width - rectangleWidth) / 2,
            y + height - rectangleHeight - 3,
            rectangleWidth,
            rectangleHeight
        );
        g.draw(rect);

        // plus inside rectangle
        Line2D.Double line = new Line2D.Double(
            rect.getCenterX(),
            rect.getY() + 2,
            rect.getCenterX(),
            rect.getMaxY() - 2
        );
        g.draw(line);
        line = new Line2D.Double(rect.getMinX() + 2, rect.getCenterY(), rect.getMaxX() - 2, rect.getCenterY());
        g.draw(line);
    }

    public void drawActivityMarkers(
        int x,
        int y,
        int width,
        int height,
        boolean multiInstanceSequential,
        boolean multiInstanceParallel,
        boolean collapsed
    ) {
        if (collapsed) {
            if (!multiInstanceSequential && !multiInstanceParallel) {
                drawCollapsedMarker(x, y, width, height);
            } else {
                drawCollapsedMarker(x - MARKER_WIDTH / 2 - 2, y, width, height);
                if (multiInstanceSequential) {
                    drawMultiInstanceMarker(true, x + MARKER_WIDTH / 2 + 2, y, width, height);
                } else {
                    drawMultiInstanceMarker(false, x + MARKER_WIDTH / 2 + 2, y, width, height);
                }
            }
        } else {
            if (multiInstanceSequential) {
                drawMultiInstanceMarker(true, x, y, width, height);
            } else if (multiInstanceParallel) {
                drawMultiInstanceMarker(false, x, y, width, height);
            }
        }
    }

    public void drawGateway(GraphicInfo graphicInfo) {
        Polygon rhombus = new Polygon();
        int x = (int) graphicInfo.getX();
        int y = (int) graphicInfo.getY();
        int width = (int) graphicInfo.getWidth();
        int height = (int) graphicInfo.getHeight();

        rhombus.addPoint(x, y + (height / 2));
        rhombus.addPoint(x + (width / 2), y + height);
        rhombus.addPoint(x + width, y + (height / 2));
        rhombus.addPoint(x + (width / 2), y);
        g.draw(rhombus);
    }

    public void drawGatewayHighLight(GraphicInfo graphicInfo, Color color) {
        Paint originalPaint = g.getPaint();
        Stroke originalStroke = g.getStroke();
        g.setPaint(color);
        g.setStroke(THICK_TASK_BORDER_STROKE);

        drawGateway(graphicInfo);

        g.setPaint(originalPaint);
        g.setStroke(originalStroke);
    }

    public void drawGatewayHighLightCompleted(GraphicInfo graphicInfo) {
        drawGatewayHighLight(graphicInfo, HIGHLIGHT_COMPLETED_ACTIVITY_COLOR);
    }

    public void drawGatewayHighLightErrored(GraphicInfo graphicInfo) {
        drawGatewayHighLight(graphicInfo, HIGHLIGHT_ERRORED_ACTIVITY_COLOR);
    }

    public void drawParallelGateway(String id, GraphicInfo graphicInfo) {
        // rhombus
        drawGateway(graphicInfo);
        int x = (int) graphicInfo.getX();
        int y = (int) graphicInfo.getY();
        int width = (int) graphicInfo.getWidth();
        int height = (int) graphicInfo.getHeight();

        // plus inside rhombus
        Stroke orginalStroke = g.getStroke();
        g.setStroke(GATEWAY_TYPE_STROKE);
        Line2D.Double line = new Line2D.Double(x + 10, y + height / 2, x + width - 10, y + height / 2); // horizontal
        g.draw(line);
        line = new Line2D.Double(x + width / 2, y + height - 10, x + width / 2, y + 10); // vertical
        g.draw(line);
        g.setStroke(orginalStroke);

        // set element's id
        g.setCurrentGroupId(id);
    }

    public void drawExclusiveGateway(String id, GraphicInfo graphicInfo) {
        // rhombus
        drawGateway(graphicInfo);
        int x = (int) graphicInfo.getX();
        int y = (int) graphicInfo.getY();
        int width = (int) graphicInfo.getWidth();
        int height = (int) graphicInfo.getHeight();

        int quarterWidth = width / 4;
        int quarterHeight = height / 4;

        // X inside rhombus
        Stroke orginalStroke = g.getStroke();
        g.setStroke(GATEWAY_TYPE_STROKE);
        Line2D.Double line = new Line2D.Double(
            x + quarterWidth + 3,
            y + quarterHeight + 3,
            x + 3 * quarterWidth - 3,
            y + 3 * quarterHeight - 3
        );
        g.draw(line);
        line = new Line2D.Double(
            x + quarterWidth + 3,
            y + 3 * quarterHeight - 3,
            x + 3 * quarterWidth - 3,
            y + quarterHeight + 3
        );
        g.draw(line);
        g.setStroke(orginalStroke);

        // set element's id
        g.setCurrentGroupId(id);
    }

    public void drawInclusiveGateway(String id, GraphicInfo graphicInfo) {
        // rhombus
        drawGateway(graphicInfo);
        int x = (int) graphicInfo.getX();
        int y = (int) graphicInfo.getY();
        int width = (int) graphicInfo.getWidth();
        int height = (int) graphicInfo.getHeight();

        int diameter = width / 2;

        // circle inside rhombus
        Stroke orginalStroke = g.getStroke();
        g.setStroke(GATEWAY_TYPE_STROKE);
        Ellipse2D.Double circle = new Ellipse2D.Double(
            ((width - diameter) / 2) + x,
            ((height - diameter) / 2) + y,
            diameter,
            diameter
        );
        g.draw(circle);
        g.setStroke(orginalStroke);

        // set element's id
        g.setCurrentGroupId(id);
    }

    public void drawEventBasedGateway(String id, GraphicInfo graphicInfo) {
        // rhombus
        drawGateway(graphicInfo);

        int x = (int) graphicInfo.getX();
        int y = (int) graphicInfo.getY();
        int width = (int) graphicInfo.getWidth();
        int height = (int) graphicInfo.getHeight();

        double scale = .6;

        GraphicInfo eventInfo = new GraphicInfo();
        eventInfo.setX(x + (width * (1 - scale)) / 2);
        eventInfo.setY(y + (height * (1 - scale)) / 2);
        eventInfo.setWidth(width * scale);
        eventInfo.setHeight(height * scale);
        drawCatchingEvent(null, eventInfo, true, null, "eventGateway");

        double r = width / 6.;

        // create pentagon (coords with respect to center)
        int topX = (int) (.95 * r); // top right corner
        int topY = (int) (-.31 * r);
        int bottomX = (int) (.59 * r); // bottom right corner
        int bottomY = (int) (.81 * r);

        int[] xPoints = new int[] { 0, topX, bottomX, -bottomX, -topX };
        int[] yPoints = new int[] { -(int) r, topY, bottomY, bottomY, topY };
        Polygon pentagon = new Polygon(xPoints, yPoints, 5);
        pentagon.translate(x + width / 2, y + width / 2);

        // draw
        g.drawPolygon(pentagon);

        // set element's id
        g.setCurrentGroupId(id);
    }

    public void drawMultiInstanceMarker(boolean sequential, int x, int y, int width, int height) {
        int rectangleWidth = MARKER_WIDTH;
        int rectangleHeight = MARKER_WIDTH;
        int lineX = x + (width - rectangleWidth) / 2;
        int lineY = y + height - rectangleHeight - 3;

        Stroke orginalStroke = g.getStroke();
        g.setStroke(MULTI_INSTANCE_STROKE);

        if (sequential) {
            g.draw(new Line2D.Double(lineX, lineY, lineX + rectangleWidth, lineY));
            g.draw(
                new Line2D.Double(
                    lineX,
                    lineY + rectangleHeight / 2,
                    lineX + rectangleWidth,
                    lineY + rectangleHeight / 2
                )
            );
            g.draw(new Line2D.Double(lineX, lineY + rectangleHeight, lineX + rectangleWidth, lineY + rectangleHeight));
        } else {
            g.draw(new Line2D.Double(lineX, lineY, lineX, lineY + rectangleHeight));
            g.draw(
                new Line2D.Double(
                    lineX + rectangleWidth / 2,
                    lineY,
                    lineX + rectangleWidth / 2,
                    lineY + rectangleHeight
                )
            );
            g.draw(new Line2D.Double(lineX + rectangleWidth, lineY, lineX + rectangleWidth, lineY + rectangleHeight));
        }

        g.setStroke(orginalStroke);
    }

    public void drawHighLightCurrent(GraphicInfo graphicInfo) {
        drawHighLight(graphicInfo, HIGHLIGHT_CURRENT_COLOR);
    }

    public void drawHighLightCompleted(GraphicInfo graphicInfo) {
        drawHighLight(graphicInfo, HIGHLIGHT_COMPLETED_ACTIVITY_COLOR);
    }

    public void drawHighLightErrored(GraphicInfo graphicInfo) {
        drawHighLight(graphicInfo, HIGHLIGHT_ERRORED_ACTIVITY_COLOR);
    }

    public void drawHighLight(GraphicInfo graphicInfo, Color color) {
        Paint originalPaint = g.getPaint();
        Stroke originalStroke = g.getStroke();

        g.setPaint(color);
        g.setStroke(THICK_TASK_BORDER_STROKE);

        RoundRectangle2D rect = new RoundRectangle2D.Double(
            (int) graphicInfo.getX(),
            (int) graphicInfo.getY(),
            (int) graphicInfo.getWidth(),
            (int) graphicInfo.getHeight(),
            6,
            6
        );
        g.draw(rect);

        g.setPaint(originalPaint);
        g.setStroke(originalStroke);
    }

    public void drawEventHighLight(GraphicInfo graphicInfo, Color color) {
        Paint originalPaint = g.getPaint();
        Stroke originalStroke = g.getStroke();

        g.setPaint(color);
        g.setStroke(THICK_TASK_BORDER_STROKE);

        Ellipse2D circle = new Ellipse2D.Double(
            (int) graphicInfo.getX(),
            (int) graphicInfo.getY(),
            (int) graphicInfo.getWidth(),
            (int) graphicInfo.getHeight()
        );

        g.draw(circle);

        g.setPaint(originalPaint);
        g.setStroke(originalStroke);
    }

    public void drawEventHighLightCompleted(GraphicInfo graphicInfo) {
        drawEventHighLight(graphicInfo, HIGHLIGHT_COMPLETED_ACTIVITY_COLOR);
    }

    public void drawEventHighLightErrored(GraphicInfo graphicInfo) {
        drawEventHighLight(graphicInfo, HIGHLIGHT_ERRORED_ACTIVITY_COLOR);
    }

    public void drawTextAnnotation(String id, String text, GraphicInfo graphicInfo) {
        int x = (int) graphicInfo.getX();
        int y = (int) graphicInfo.getY();
        int width = (int) graphicInfo.getWidth();
        int height = (int) graphicInfo.getHeight();

        Font originalFont = g.getFont();
        Stroke originalStroke = g.getStroke();

        g.setFont(ANNOTATION_FONT);

        Path2D path = new Path2D.Double();
        x += .5;
        int lineLength = 18;
        path.moveTo(x + lineLength, y);
        path.lineTo(x, y);
        path.lineTo(x, y + height);
        path.lineTo(x + lineLength, y + height);

        path.lineTo(x + lineLength, y + height - 1);
        path.lineTo(x + 1, y + height - 1);
        path.lineTo(x + 1, y + 1);
        path.lineTo(x + lineLength, y + 1);
        path.closePath();

        g.draw(path);

        int boxWidth = width - (2 * ANNOTATION_TEXT_PADDING);
        int boxHeight = height - (2 * ANNOTATION_TEXT_PADDING);
        int boxX = x + width / 2 - boxWidth / 2;
        int boxY = y + height / 2 - boxHeight / 2;

        if (text != null && !text.isEmpty()) {
            drawMultilineAnnotationText(text, boxX, boxY, boxWidth, boxHeight);
        }

        // restore originals
        g.setFont(originalFont);
        g.setStroke(originalStroke);

        // set element's id
        g.setCurrentGroupId(id);
    }

    public void drawLabel(String text, GraphicInfo graphicInfo) {
        drawLabel(text, graphicInfo, true);
    }

    public void drawLabel(String text, GraphicInfo graphicInfo, boolean centered) {
        float interline = 1.0f;

        // text
        if (text != null && text.length() > 0) {
            Paint originalPaint = g.getPaint();
            Font originalFont = g.getFont();

            g.setPaint(LABEL_COLOR);
            g.setFont(LABEL_FONT);

            int wrapWidth = 100;
            int textY = (int) graphicInfo.getY();

            // TODO: use drawMultilineText()
            AttributedString as = new AttributedString(text);
            as.addAttribute(TextAttribute.FOREGROUND, g.getPaint());
            as.addAttribute(TextAttribute.FONT, g.getFont());
            AttributedCharacterIterator aci = as.getIterator();
            FontRenderContext frc = new FontRenderContext(null, true, false);
            LineBreakMeasurer lbm = new LineBreakMeasurer(aci, frc);

            while (lbm.getPosition() < text.length()) {
                TextLayout tl = lbm.nextLayout(wrapWidth);
                textY += tl.getAscent();
                Rectangle2D bb = tl.getBounds();
                double tX = graphicInfo.getX();
                if (centered) {
                    tX += (int) (graphicInfo.getWidth() / 2 - bb.getWidth() / 2);
                }
                tl.draw(g, (float) tX, textY);
                textY += tl.getDescent() + tl.getLeading() + (interline - 1.0f) * tl.getAscent();
            }

            // restore originals
            g.setFont(originalFont);
            g.setPaint(originalPaint);
        }
    }

    /**
     * This method makes coordinates of connection flow better.
     * @param sourceShapeType
     * @param targetShapeType
     * @param sourceGraphicInfo
     * @param targetGraphicInfo
     * @param graphicInfoList
     */
    public List<GraphicInfo> connectionPerfectionizer(
        SHAPE_TYPE sourceShapeType,
        SHAPE_TYPE targetShapeType,
        GraphicInfo sourceGraphicInfo,
        GraphicInfo targetGraphicInfo,
        List<GraphicInfo> graphicInfoList
    ) {
        Shape shapeFirst = createShape(sourceShapeType, sourceGraphicInfo);
        Shape shapeLast = createShape(targetShapeType, targetGraphicInfo);

        if (graphicInfoList != null && graphicInfoList.size() > 0) {
            GraphicInfo graphicInfoFirst = graphicInfoList.get(0);
            GraphicInfo graphicInfoLast = graphicInfoList.get(graphicInfoList.size() - 1);
            if (shapeFirst != null) {
                graphicInfoFirst.setX(shapeFirst.getBounds2D().getCenterX());
                graphicInfoFirst.setY(shapeFirst.getBounds2D().getCenterY());
            }
            if (shapeLast != null) {
                graphicInfoLast.setX(shapeLast.getBounds2D().getCenterX());
                graphicInfoLast.setY(shapeLast.getBounds2D().getCenterY());
            }

            Point p = null;

            if (shapeFirst != null) {
                Line2D.Double lineFirst = new Line2D.Double(
                    graphicInfoFirst.getX(),
                    graphicInfoFirst.getY(),
                    graphicInfoList.get(1).getX(),
                    graphicInfoList.get(1).getY()
                );
                p = getIntersection(shapeFirst, lineFirst);
                if (p != null) {
                    graphicInfoFirst.setX(p.getX());
                    graphicInfoFirst.setY(p.getY());
                }
            }

            if (shapeLast != null) {
                Line2D.Double lineLast = new Line2D.Double(
                    graphicInfoLast.getX(),
                    graphicInfoLast.getY(),
                    graphicInfoList.get(graphicInfoList.size() - 2).getX(),
                    graphicInfoList.get(graphicInfoList.size() - 2).getY()
                );
                p = getIntersection(shapeLast, lineLast);
                if (p != null) {
                    graphicInfoLast.setX(p.getX());
                    graphicInfoLast.setY(p.getY());
                }
            }
        }

        return graphicInfoList;
    }

    /**
     * This method creates shape by type and coordinates.
     * @param shapeType
     * @param graphicInfo
     * @return Shape
     */
    private static Shape createShape(SHAPE_TYPE shapeType, GraphicInfo graphicInfo) {
        if (SHAPE_TYPE.Rectangle.equals(shapeType)) {
            // source is rectangle
            return new Rectangle2D.Double(
                graphicInfo.getX(),
                graphicInfo.getY(),
                graphicInfo.getWidth(),
                graphicInfo.getHeight()
            );
        } else if (SHAPE_TYPE.Rhombus.equals(shapeType)) {
            // source is rhombus
            Path2D.Double rhombus = new Path2D.Double();
            rhombus.moveTo(graphicInfo.getX(), graphicInfo.getY() + graphicInfo.getHeight() / 2);
            rhombus.lineTo(
                graphicInfo.getX() + graphicInfo.getWidth() / 2,
                graphicInfo.getY() + graphicInfo.getHeight()
            );
            rhombus.lineTo(
                graphicInfo.getX() + graphicInfo.getWidth(),
                graphicInfo.getY() + graphicInfo.getHeight() / 2
            );
            rhombus.lineTo(graphicInfo.getX() + graphicInfo.getWidth() / 2, graphicInfo.getY());
            rhombus.lineTo(graphicInfo.getX(), graphicInfo.getY() + graphicInfo.getHeight() / 2);
            rhombus.closePath();
            return rhombus;
        } else if (SHAPE_TYPE.Ellipse.equals(shapeType)) {
            // source is ellipse
            return new Ellipse2D.Double(
                graphicInfo.getX(),
                graphicInfo.getY(),
                graphicInfo.getWidth(),
                graphicInfo.getHeight()
            );
        }
        // unknown source element, just do not correct coordinates
        return null;
    }

    /**
     * This method returns intersection point of shape border and line.
     * @param shape
     * @param line
     * @return Point
     */
    private static Point getIntersection(Shape shape, Line2D.Double line) {
        if (shape instanceof Ellipse2D) {
            return getEllipseIntersection(shape, line);
        } else if (shape instanceof Rectangle2D || shape instanceof Path2D) {
            return getShapeIntersection(shape, line);
        } else {
            // something strange
            return null;
        }
    }

    /**
     * This method calculates ellipse intersection with line
     * @param shape Bounds of this shape used to calculate parameters of inscribed into this bounds ellipse.
     * @param line
     * @return Intersection point
     */
    private static Point getEllipseIntersection(Shape shape, Line2D.Double line) {
        double angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1);
        double x = (shape.getBounds2D().getWidth() / 2) * Math.cos(angle) + shape.getBounds2D().getCenterX();
        double y = (shape.getBounds2D().getHeight() / 2) * Math.sin(angle) + shape.getBounds2D().getCenterY();
        Point p = new Point();
        p.setLocation(x, y);
        return p;
    }

    /**
     * This method calculates shape intersection with line.
     * @param shape
     * @param line
     * @return Intersection point
     */
    private static Point getShapeIntersection(Shape shape, Line2D.Double line) {
        PathIterator it = shape.getPathIterator(null);
        double[] coords = new double[6];
        double[] pos = new double[2];
        Line2D.Double l = new Line2D.Double();
        while (!it.isDone()) {
            int type = it.currentSegment(coords);
            switch (type) {
                case PathIterator.SEG_MOVETO:
                    pos[0] = coords[0];
                    pos[1] = coords[1];
                    break;
                case PathIterator.SEG_LINETO:
                    l = new Line2D.Double(pos[0], pos[1], coords[0], coords[1]);
                    if (line.intersectsLine(l)) {
                        return getLinesIntersection(line, l);
                    }
                    pos[0] = coords[0];
                    pos[1] = coords[1];
                    break;
                case PathIterator.SEG_CLOSE:
                    break;
                default:
                // whatever
            }
            it.next();
        }
        return null;
    }

    /**
     * This method calculates intersections of two lines.
     * @param a Line 1
     * @param b Line 2
     * @return Intersection point
     */
    private static Point getLinesIntersection(Line2D a, Line2D b) {
        double d =
            (a.getX1() - a.getX2()) * (b.getY2() - b.getY1()) - (a.getY1() - a.getY2()) * (b.getX2() - b.getX1());
        double da =
            (a.getX1() - b.getX1()) * (b.getY2() - b.getY1()) - (a.getY1() - b.getY1()) * (b.getX2() - b.getX1());
        double ta = da / d;
        Point p = new Point();
        p.setLocation(a.getX1() + ta * (a.getX2() - a.getX1()), a.getY1() + ta * (a.getY2() - a.getY1()));
        return p;
    }
}
